Skip to content

feat(career): 職務経歴書 保存時の文章校正(誤字脱字・表記ゆれ)#309

Merged
yusuke0610 merged 4 commits into
mainfrom
claude/resume-text-validation-5gEtW
Jun 6, 2026
Merged

feat(career): 職務経歴書 保存時の文章校正(誤字脱字・表記ゆれ)#309
yusuke0610 merged 4 commits into
mainfrom
claude/resume-text-validation-5gEtW

Conversation

@yusuke0610

@yusuke0610 yusuke0610 commented Jun 6, 2026

Copy link
Copy Markdown
Owner

概要

職務経歴書の保存確認ダイアログ(CareerDiffModal)に 文章校正(誤字脱字・表記ゆれ・冗長表現) を追加します。保存前に編集中フォームの全テキスト項目を校正し、PDF レイアウトと同じ並びで指摘を表示します。警告のみで保存はブロックしません。

転職で人の目に触れる職務経歴書に、提出前のセルフチェック手段を提供するのが目的です。

方針(ADR-0009)

  • ルールベース・フロントエンド完結(Web Worker): 個人情報を外部 API / LLM に送信しない。コストゼロ・オフライン。
  • エンジン: @textlint/kernel + textlint-rule-preset-ja-technical-writing(全23ルール)+ prh(IT 用語の表記ゆれ辞書を同梱)。
  • kuromoji 辞書(形態素解析・約18MB)を frontend/public/kuromoji-dict/ に静的配信。辞書ロード失敗時は形態素解析依存ルールを除外して prh + 非依存ルールで継続(グレースフルデグラデーション)。
  • textlint / kuromoji 一式は動的 import で worker チャンクへ分割し、初期バンドル・初回描画から切り離す。

UI

  • 校正は保存確認ダイアログ表示中だけ起動(useProofread)。新規作成の初回保存でもダイアログを開き、取りこぼしを防ぐ。
  • 左右ペイン(PDF レイアウト)にも青の波線でマーキング。差分の背景色(黄/緑/赤)と重ねても潰れないよう下線で表現。
  • 右サイドバーは「変更点」「校正」を分けず、フィールド単位の1本のレビュー一覧に統合buildReviewEntries)。PDF レイアウト順(氏名→職務要約→職歴→資格→自己PR)に並べ、左右ペインと縦順を一致させて上から突合できる。

主な変更

  • 新規 frontend/src/proofread/(worker / client / collectCareerTexts / issueFormat / prh辞書 / polyfill)、hooks/career/useProofread.tsutils/careerReview.tspublic/kuromoji-dict/public/_headers
  • CareerDiffModal / CareerResumeForm / diffHighlight.ts / careerDiff.ts / messages.ts / vite.config.ts を更新。
  • ADR-0009 を追加。

ブラウザ統合のポイント(worker でのハマりどころ)

  • window / process(cwd/nextTick) の最小ポリフィル、node 組込(path/os/assert)のブラウザ実装エイリアス。
  • kuromoji 辞書 URL をルート相対に(path.join:// を潰す問題回避)。
  • 辞書 .gz二重 gzip 展開させない配信(dev: Vite プラグイン / prod: public/_headers)。

検証

  • make ci 相当を pass: ESLint / lint-frontend-messages / ユニット 295 pass(新規テスト追加)/ build。
  • npm audit --audit-level=high: high/critical なし。
  • E2E 全28 pass(新規 career-proofread.spec.ts で 実ブラウザ → worker → kuromoji辞書ロード → prh指摘表示まで検証)。

留意点

  • kuromoji 辞書 約18MB を public/ にコミットしています(リポジトリ履歴が増加。ADR-0009 にトレードオフと将来の CDN 移行条件を記載)。
  • 校正メッセージは ja-technical-writing 仕様で「理由/修正/例」を含むため、やや長め。必要なら要約表示に調整可能です。

https://claude.ai/code/session_01QANbvc5WkCDYDNyZw8gUnx


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Proofreading integrated into the career save confirmation: inline suggestions for writing/IT terminology appear but do not block saving; graceful degradation if dictionary load fails.
  • Documentation
    • Added an architectural decision record describing the proofreading approach.
  • Style
    • Updated save-confirmation modal layout and added styles for proofreading items and loading states.
  • Tests
    • Added E2E and unit tests covering proofreading behavior and review merging.

claude added 3 commits June 6, 2026 06:21
保存確認ダイアログ(CareerDiffModal)に「校正の指摘」セクションを追加し、
編集中フォームの全テキスト項目を textlint + prh で校正して表示する。
保存はブロックせず警告のみ。指摘は右サイドバー下部に青系・控えめで表示する。

- 校正エンジンは Web Worker(src/proofread/)でフロント完結。textlint kernel +
  技術文書プリセット全23ルール + prh(IT表記ゆれ辞書同梱)。個人情報を外部送信しない。
- kuromoji 辞書(public/kuromoji-dict/)は静的配信。辞書ロード失敗時は形態素解析依存
  ルールを除外して prh + 非依存ルールで継続(グレースフルデグラデーション)。
- useProofread フックがダイアログ表示中だけ校正を起動。初回保存(新規・差分無し)も
  空フォーム基準の差分算出によりダイアログを開き、校正の取りこぼしを防ぐ。
- worker のブラウザ統合: window/process 最小ポリフィル、node 組込(path/os/assert)の
  ブラウザ実装エイリアス、辞書 .gz を二重 gzip させない配信(dev プラグイン / _headers)。
- ADR-0009 を追加。
- 左右 diff の編集中ペインで、校正指摘のあるフィールド(data-fp 一致)に青の波線を
  付与(diffHighlight の annotateHtml/foldUnchanged を拡張)。差分の背景色(黄/緑/赤)と
  重ねても潰れないよう下線で表現し、指摘のある項目は折りたたみから除外する。
- 変更点リストと校正リストの並びを PDF レイアウト順(氏名→職務要約→職務経歴→資格→自己PR)
  に統一。buildCareerChanges / collectCareerTexts で self_pr を末尾へ移動し、左右ペインと
  サイドバーの縦順を一致させて突合しやすくした。
ユーザーレビューを受けた改善:
- 校正指摘を左右ペイン(PDFレイアウト)にも青の波線でマーキング。差分の背景色
  (黄/緑/赤)と重ねても潰れないよう下線で表現。校正指摘のある項目は折りたたみから除外。
- 変更点・校正の並び順を PDF レイアウト順(氏名→職務要約→職歴→資格→自己PR)に統一。
  buildCareerChanges / collectCareerTexts の self_pr を末尾へ移動。
- 右サイドバーの「変更点」「校正」2セクション分割を廃止し、フィールド単位の
  1本のレビュー一覧(buildReviewEntries)に統合。各カードに差分と校正を併記し、
  左右ペインと縦順を一致させて上から突合できるようにした。
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Need an answer fast? Review this PR in Change Stack to ask focused questions about the PR or a changed range.

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3475137f-290f-43b6-9c00-a77114ebbf79

📥 Commits

Reviewing files that changed from the base of the PR and between 2bb8237 and 7d410fa.

📒 Files selected for processing (4)
  • docs/adr/0009-frontend-textlint-proofread.md
  • frontend/src/components/forms/CareerDiffModal.module.css
  • frontend/src/utils/careerReview.ts
  • frontend/vite.config.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/adr/0009-frontend-textlint-proofread.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/utils/careerReview.ts
  • frontend/vite.config.ts
  • frontend/src/components/forms/CareerDiffModal.module.css

📝 Walkthrough

Walkthrough

This PR adds end-to-end Japanese text proofreading to the career form save flow. When users save their résumé, a Web Worker runs textlint with Japanese technical-writing rules to detect spelling and style issues, displays them in the save-confirmation dialog alongside diffs, and does not block saving. The implementation includes graceful fallback if kuromoji dictionary resources fail to load.

Changes

Frontend proofreading via Web Worker

Layer / File(s) Summary
Proofread types and module declarations
frontend/src/proofread/types.ts, frontend/src/proofread/textlint-modules.d.ts
Defines ProofreadSeverity, CareerTextItem, ProofreadIssue, and worker request/response message contracts; adds ambient module declaration for textlint-rule-*.
Worker environment, polyfills and kernel setup
frontend/src/proofread/worker-env-polyfill.ts, frontend/src/proofread/proofread.worker.ts, frontend/src/proofread/prh-it-terms.yml
Polyfills worker globals, sets kuromoji dictionary path, implements lazy textlint kernel with dynamic imports, attempts kuromojin preload, conditionally excludes kuromoji-dependent rules, and includes prh YAML IT-terms ruleset.
Worker client API
frontend/src/proofread/proofreadClient.ts
Lazily creates/caches a Worker, posts ProofreadRequest with requestId and items, resolves/rejects on matching worker response or error, and short-circuits when no items are present.
Text collection, formatting and grouping
frontend/src/proofread/collectCareerTexts.ts, frontend/src/proofread/collectCareerTexts.test.ts, frontend/src/proofread/issueFormat.ts, frontend/src/proofread/issueFormat.test.ts
Flattens saved free-text fields from CareerFormState in layout order, trims/filters out blanks, builds labels/ids, maps numeric severities to strings, builds truncated excerpts, and groups issues by field with tests.
Reactive proofreading hook
frontend/src/hooks/career/useProofread.ts, frontend/src/hooks/career/useProofread.test.ts
Provides a debounced (300ms) React hook that calls the proofread client, manages issues/proofreading/error state, cancels on disable/unmount, and includes unit tests for success, failure, disable, and toggle behaviors.
Review entry merging and ordering
frontend/src/utils/careerReview.ts, frontend/src/utils/careerReview.test.ts
Merges career diffs and proofread issues into per-path ReviewEntry items, defines top-level and container ordering to match PDF layout, and sorts entries deterministically; includes tests.
Diff annotation and folding changes
frontend/src/utils/diffHighlight.ts, frontend/src/utils/diffHighlight.test.ts
Extends annotateHtml to accept proofread field ids and add diff-proofread class, adds unitHasProofread, and updates foldUnchanged to avoid collapsing units that contain proofread fields; tests updated accordingly.
UI integration and styling
frontend/src/components/forms/CareerDiffModal.tsx, frontend/src/components/forms/CareerDiffModal.module.css, frontend/src/components/forms/CareerResumeForm.tsx, frontend/src/constants/messages.ts
CareerDiffModal accepts and displays proofreading issues alongside diffs, renders combined review entries, shows loading/empty/error states, and CareerResumeForm invokes useProofread and passes results; CSS adds proofread visuals and messages constants added.
Build, deps and static-file handling
frontend/vite.config.ts, frontend/package.json, frontend/public/_headers, frontend/src/utils/careerDiff.ts
Adds Vite middleware to serve kuromoji .dat.gz as raw octets, maps Node built-ins to browser shims, sets worker.format to ES, updates frontend deps to include textlint and browser polyfills, adds Cloudflare _headers for immutable cache, and reorders some diff output for layout alignment.
Documentation and e2e test
docs/adr/0009-frontend-textlint-proofread.md, frontend/e2e/career-proofread.spec.ts
ADR documents architecture, risks, and graceful-degradation strategy. E2E test mocks resume APIs, verifies proofread suggestion appears in save modal and that saving remains enabled.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • yusuke0610/devforge#306: Modifies the diff preview layer (CareerDiffModal and diffHighlight utilities); this PR extends those same files to support proofreading field markers.

"🐰
I nibble lines of code at night,
A worker hums, the text made right.
Blue waves beneath each word I find,
Save with a hop — no block, just kind."

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature: adding proofread functionality (typos, spelling variants) when saving career forms, which aligns with the primary objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 82.14% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/resume-text-validation-5gEtW

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
frontend/e2e/career-proofread.spec.ts (1)

59-71: ⚡ Quick win

Use SSoT/test constants instead of inline Japanese message literals in selectors.

Lines 59–71 directly embed UI strings ("例: 山田 太郎", "変更内容の確認", "変更点・校正", "この内容で保存"). Please route these through a shared message source (or stable test IDs) so copy updates don’t silently break E2E.

As per coding guidelines, “In frontend code, follow message management rules … message literals are forbidden and must use Single Source of Truth (SSoT)”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/career-proofread.spec.ts` around lines 59 - 71, The test embeds
UI message literals directly in selectors (page.getByPlaceholder("例: 山田 太郎"),
page.getByRole(..., { name: "変更内容の確認" }), page.getByText("変更点・校正"),
page.getByRole(..., { name: "この内容で保存" })), violating the SSoT rule; update the
spec to import and use the shared message/test constants (or stable test IDs)
instead of inline Japanese strings for those calls, replacing the literal
arguments with the appropriate SSoT constants (or test-id lookups) so UI text
changes won’t break frontend/e2e/career-proofread.spec.ts.

Source: Coding guidelines

frontend/src/utils/careerReview.ts (1)

56-76: 💤 Low value

Consider extracting magic numbers to named constants.

The values 400 (line 59) and 500 (line 75) are used to rank unknown fields for stable sorting. For maintainability, consider extracting these as named constants.

♻️ Suggested refactor
+/** Unknown top-level fields are sorted after known fields, using charCode for stability. */
+const UNKNOWN_TOP_LEVEL_BASE = 400;
+/** Unknown container fields are sorted after known fields within their container. */
+const UNKNOWN_CONTAINER_FIELD_BASE = 500;
+
 /** 既知の並びにあればその index、無ければ末尾側(文字コードで安定ソート)に寄せる。 */
 function orderIndex(list: string[], seg: string): number {
   const i = list.indexOf(seg);
-  return i >= 0 ? i : 400 + (seg.charCodeAt(0) || 0);
+  return i >= 0 ? i : UNKNOWN_TOP_LEVEL_BASE + (seg.charCodeAt(0) || 0);
 }
 
 /**
  * パスを「並び順を表す数値タプル」に変換する。
  * - 先頭セグメント: トップレベル順
  * - 数値セグメント: 配列 index
  * - 名前付きセグメント: 直近の配列名(2 つ前)から決まるコンテナのフィールド順
  */
 function rankTuple(path: string): number[] {
   const segs = path.split(".");
   return segs.map((seg, i) => {
     if (i === 0) return orderIndex(TOP_ORDER, seg);
     if (/^\d+$/.test(seg)) return Number(seg);
     const containerName = i >= 2 && /^\d+$/.test(segs[i - 1]) ? segs[i - 2] : null;
     const order = containerName ? CONTAINER_FIELD_ORDER[containerName] : null;
-    return order ? orderIndex(order, seg) : 500;
+    return order ? orderIndex(order, seg) : UNKNOWN_CONTAINER_FIELD_BASE;
   });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/careerReview.ts` around lines 56 - 76, The code uses magic
numbers 400 and 500 in orderIndex and rankTuple; extract them into named
constants (e.g., UNKNOWN_TOP_ORDER_BASE = 400 and UNKNOWN_FIELD_ORDER = 500) and
replace the literal usages in orderIndex (return i >= 0 ? i :
UNKNOWN_TOP_ORDER_BASE + (seg.charCodeAt(0) || 0)) and in rankTuple’s default
return (return order ? orderIndex(order, seg) : UNKNOWN_FIELD_ORDER) so the
intent is clear and easy to maintain; also add brief comments above the new
constants explaining their purpose.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/adr/0009-frontend-textlint-proofread.md`:
- Line 25: ADR text and implementation disagree on the number of
morphology-dependent rules excluded on dictionary-load failure; update the ADR
to match the worker implementation by changing “10ルール” to “11ルール” (or explicitly
list the rules) so it reflects the actual KUROMOJI_RULE_IDS set (which currently
includes "arabic-kanji-numbers"); refer to the KUROMOJI_RULE_IDS identifier and
the "arabic-kanji-numbers" rule when making the ADR edit to ensure counts and/or
listed rules align.

In `@frontend/src/components/forms/CareerDiffModal.module.css`:
- Line 287: Replace the deprecated CSS declaration "word-break: break-word" in
CareerDiffModal.module.css with the modern equivalent; locate both occurrences
of the exact token "word-break: break-word" and change them to "overflow-wrap:
break-word" (or to "word-break: break-all" only if you need more aggressive
breaking), ensuring the replacement preserves existing selectors/styles that
contain these declarations.

In `@frontend/vite.config.ts`:
- Around line 27-28: The middleware builds filePath using
normalize(join(publicDir, url)) which fails when url starts with "/" because
join treats it as absolute; update the construction to ensure the url is treated
as relative (e.g., strip the leading slash or prefix with "./") before joining
so filePath correctly begins with publicDir, then keep the existing
startsWith(publicDir) check; locate the filePath variable creation and change
the normalize(join(publicDir, url)) expression (and any related checks) to use a
relative-safe join such as joining publicDir with url.slice(1) or '.' + url so
requests under "/kuromoji-dict/" are not treated as absolute paths.

---

Nitpick comments:
In `@frontend/e2e/career-proofread.spec.ts`:
- Around line 59-71: The test embeds UI message literals directly in selectors
(page.getByPlaceholder("例: 山田 太郎"), page.getByRole(..., { name: "変更内容の確認" }),
page.getByText("変更点・校正"), page.getByRole(..., { name: "この内容で保存" })), violating
the SSoT rule; update the spec to import and use the shared message/test
constants (or stable test IDs) instead of inline Japanese strings for those
calls, replacing the literal arguments with the appropriate SSoT constants (or
test-id lookups) so UI text changes won’t break
frontend/e2e/career-proofread.spec.ts.

In `@frontend/src/utils/careerReview.ts`:
- Around line 56-76: The code uses magic numbers 400 and 500 in orderIndex and
rankTuple; extract them into named constants (e.g., UNKNOWN_TOP_ORDER_BASE = 400
and UNKNOWN_FIELD_ORDER = 500) and replace the literal usages in orderIndex
(return i >= 0 ? i : UNKNOWN_TOP_ORDER_BASE + (seg.charCodeAt(0) || 0)) and in
rankTuple’s default return (return order ? orderIndex(order, seg) :
UNKNOWN_FIELD_ORDER) so the intent is clear and easy to maintain; also add brief
comments above the new constants explaining their purpose.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d82d332-d0d8-4587-be80-c8dfede552a5

📥 Commits

Reviewing files that changed from the base of the PR and between d6e9a1b and 2bb8237.

⛔ Files ignored due to path filters (13)
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • frontend/public/kuromoji-dict/base.dat.gz is excluded by !**/*.gz
  • frontend/public/kuromoji-dict/cc.dat.gz is excluded by !**/*.gz
  • frontend/public/kuromoji-dict/check.dat.gz is excluded by !**/*.gz
  • frontend/public/kuromoji-dict/tid.dat.gz is excluded by !**/*.gz
  • frontend/public/kuromoji-dict/tid_map.dat.gz is excluded by !**/*.gz
  • frontend/public/kuromoji-dict/tid_pos.dat.gz is excluded by !**/*.gz
  • frontend/public/kuromoji-dict/unk.dat.gz is excluded by !**/*.gz
  • frontend/public/kuromoji-dict/unk_char.dat.gz is excluded by !**/*.gz
  • frontend/public/kuromoji-dict/unk_compat.dat.gz is excluded by !**/*.gz
  • frontend/public/kuromoji-dict/unk_invoke.dat.gz is excluded by !**/*.gz
  • frontend/public/kuromoji-dict/unk_map.dat.gz is excluded by !**/*.gz
  • frontend/public/kuromoji-dict/unk_pos.dat.gz is excluded by !**/*.gz
📒 Files selected for processing (26)
  • docs/adr/0009-frontend-textlint-proofread.md
  • frontend/e2e/career-proofread.spec.ts
  • frontend/package.json
  • frontend/public/_headers
  • frontend/src/components/forms/CareerDiffModal.module.css
  • frontend/src/components/forms/CareerDiffModal.tsx
  • frontend/src/components/forms/CareerResumeForm.tsx
  • frontend/src/constants/messages.ts
  • frontend/src/hooks/career/useProofread.test.ts
  • frontend/src/hooks/career/useProofread.ts
  • frontend/src/proofread/collectCareerTexts.test.ts
  • frontend/src/proofread/collectCareerTexts.ts
  • frontend/src/proofread/issueFormat.test.ts
  • frontend/src/proofread/issueFormat.ts
  • frontend/src/proofread/prh-it-terms.yml
  • frontend/src/proofread/proofread.worker.ts
  • frontend/src/proofread/proofreadClient.ts
  • frontend/src/proofread/textlint-modules.d.ts
  • frontend/src/proofread/types.ts
  • frontend/src/proofread/worker-env-polyfill.ts
  • frontend/src/utils/careerDiff.ts
  • frontend/src/utils/careerReview.test.ts
  • frontend/src/utils/careerReview.ts
  • frontend/src/utils/diffHighlight.test.ts
  • frontend/src/utils/diffHighlight.ts
  • frontend/vite.config.ts

Comment thread docs/adr/0009-frontend-textlint-proofread.md Outdated
Comment thread frontend/src/components/forms/CareerDiffModal.module.css Outdated
Comment thread frontend/vite.config.ts Outdated
- ADR-0009: 辞書ロード失敗時に除外する形態素依存ルール数を 10→11 に修正
  (KUROMOJI_RULE_IDS は arabic-kanji-numbers を含む実装と整合)。
- CareerDiffModal.module.css: 非推奨の word-break: break-word を
  overflow-wrap: break-word に統一(stylelint 指摘)。
- vite.config.ts: 辞書配信ミドルウェアで先頭スラッシュを除去してから join し、
  publicDir 相対解決を明示(join の絶対パス扱いを防御。挙動は従来どおり)。
- careerReview.ts: 並び順ランクのマジックナンバー(400/500)を名前付き定数化。
@yusuke0610
yusuke0610 merged commit f2f3513 into main Jun 6, 2026
18 checks passed
@yusuke0610
yusuke0610 deleted the claude/resume-text-validation-5gEtW branch June 10, 2026 05:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants